﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic;
using System.Text;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Sustainalytics.Entities;
using Sustainalytics.Utils;

namespace Sustainalytics.DataLayer
{
    /// <summary>
    /// News repository.
    /// </summary>
    public class NewsRepository : INewsRepository
    {
        private IMongoDatabase _database { get; set; }


        private void ValidateDatabase()
        {
            if (UnitTestDetector.IsInUnitTest)
                return;

            var indexKeyDefinitions = new List<IndexKeysDefinition<News>>()
            {
                Builders<News>.IndexKeys.Ascending(x => x.TitleLowercase),
                Builders<News>.IndexKeys.Ascending(x => x.Category),
                Builders<News>.IndexKeys.Ascending(x => x.AddedByUserName),
                Builders<News>.IndexKeys.Ascending(x => x.AvailableFor),
                Builders<News>.IndexKeys.Ascending(x => x.Status),
                Builders<News>.IndexKeys.Ascending(x => x.DateCreated),
                Builders<News>.IndexKeys.Ascending(x => x.UnpublishedDate),
                Builders<News>.IndexKeys.Ascending("Tags.Name"),
                Builders<News>.IndexKeys.Ascending(x => x.Category).Ascending(x=> x.TitleLowercase).Descending(x=> x.DateCreated),
                Builders<News>.IndexKeys.Ascending(x => x.Category).Ascending("Tags.Name").Descending(x=> x.DateCreated),
            };

            indexKeyDefinitions.ForEach(x =>
            {
                _database.GetCollection<News>().Indexes.CreateOne(x);
            });
        }


        /// <summary>
        /// Ctor.
        /// </summary>
        private NewsRepository(string connectionString)
        {
            // DB address
            MongoUrl mongoUrl = MongoUrl.Create(connectionString);

            if (string.IsNullOrWhiteSpace(mongoUrl.DatabaseName))
            {
                throw new ArgumentException("No database specified in the connection string!");
            }

            // Mongo DB client
            var _client = new MongoClient(mongoUrl);

            // Mongo DB
            _database = _client.GetDatabase(mongoUrl.DatabaseName);

            // validate DB
            ValidateDatabase();
        }

        /// <summary>
        /// Repository factory method.
        /// </summary>
        /// <param name="connectionString"> ClientDW Mongo DB connection string. </param>
        /// <returns> Repository instance. </returns>
        public static INewsRepository GetNewsRepository(string connectionString)
        {
            NewsRepository repo = new NewsRepository(connectionString);

            return repo;
        }

        /// <summary>
        /// Get news paged list.
        /// </summary>
        /// <param name="filter"> News filter. </param>
        /// <param name="pagingOptions"> Paging options. </param>
        /// <returns> News paged list. </returns>
        public List<News> GetNews(NewsFilter filter, int? skip = null, int? take = null, string[] orderBy = null, bool orderByAsc = true)
        {
            filter = filter ?? new NewsFilter();

            int skipVal = skip ?? 0;
            int takeVal = take ?? 100;

            var query = _database.GetCollection<News>("News").AsQueryable().Filter(filter);

            if (orderBy != null && orderBy.Length > 0)
            {
                orderBy.ToList().ForEach(o =>
                    {
                        string orderByClause = string.Format("{0} {1}", o, orderByAsc ? "ASC" : "DESC");
                        query = query.OrderBy(orderByClause);
                    });
            }

            List<News> newsLst = query.Skip(skipVal).Take(takeVal).ToList();

            return newsLst;
        }

        /// <summary>
        /// Get filtered news count.
        /// </summary>
        /// <param name="filter"> News filter. </param>
        /// <returns> Filtered news count. </returns>
        public int GetNewsCount(NewsFilter filter)
        {
            filter = filter ?? new NewsFilter();

            var query = _database.GetCollection<News>("News").AsQueryable().Filter(filter);

            int cnt = query.Count();

            return cnt;
        }

        /// <summary>
        /// Get news ID.
        /// </summary>
        /// <param name="newsId"> News ID. </param>
        /// <returns> News. </returns>
        public News GetNews(Guid newsId)
        {
            News news = (from newsItems in _database.GetCollection<News>("News").AsQueryable()
                         where newsItems.Id == newsId
                         select newsItems).FirstOrDefault();

            return news;
        }

        /// <summary>
        /// Get multiple news by IDs.
        /// </summary>
        /// <param name="newsIds"> News IDs. </param>
        /// <returns> News list. </returns>
        public IEnumerable<News> GetNewsByIds(IEnumerable<Guid> newsIds)
        {
            if (newsIds == null)
            {
                throw new DataNewsIdsNullException();
            }

            // create query to filter by provided news GUIDs
            var newsItemsQuery = Builders<News>.Filter.In(n => n.Id, newsIds);

            // get all news
            return _database.GetCollection<News>("News").Find(newsItemsQuery).ToList();
        }

        /// <summary>
        /// Add news.
        /// </summary>
        /// <param name="news"> News data. </param>
        /// <returns> Added news. </returns>
        public void AddNews(News news)
        {
            if (news == null)
            {
                throw new DataNewsNullException();
            }

            // add news
            _database.GetCollection<News>("News").InsertOne(news);
        }

        /// <summary>
        /// Update news.
        /// </summary>
        /// <param name="news"> News data. </param>
        /// <returns> Updated news. </returns>
        public void UpdateNews(News news)
        {
            if (news == null)
            {
                throw new DataNewsNullException();
            }

            _database.GetCollection<News>("News")
                .FindOneAndReplace(Builders<News>.Filter.Eq(x => x.Id, news.Id), news);
        }

        public virtual bool UpdateNewsToUnpublished(List<Tuple<Guid, NewsStatus>> list)
        {
            if (list == null)
                throw new ArgumentNullException("list");

            list.ForEach(x =>
            {
                _database.GetCollection<News>("News")
                    .FindOneAndUpdate(
                        Builders<News>.Filter.Eq(n => n.Id, x.Item1), 
                        Builders<News>.Update
                            .Set(n=> n.Status, x.Item2)
                            .Set(n=> n.StatusText, "Unpublished")
                            .Set(n=> n.StatusTextLowercase, "unpublished"));
            });
            return true;
        }

        /// <summary>
        /// Delete news.
        /// </summary>
        /// <param name="newsId"> News ID to delete. </param>
        public void DeleteNews(Guid newsId)
        {
            _database.GetCollection<News>("News").DeleteOne(Builders<News>.Filter.Eq(n=> n.Id, newsId));
        }

        /// <summary>
        /// Delete multiple news.
        /// </summary>
        /// <param name="newsItems"> News items to delete. </param>
        public void DeleteMultipleNews(IEnumerable<Guid> newsItems)
        {
            _database.GetCollection<News>("News").DeleteMany(Builders<News>.Filter.In(n => n.Id, newsItems));
        }

        public virtual List<Guid> ReadUnpublishedWithStatusActive(DateTime reference)
        {
            if (reference == null)
                throw new ArgumentNullException("reference");

            var query = _database.GetCollection<News>("News").AsQueryable()
                .Where(x => x.UnpublishedDate <= reference && x.Status == NewsStatus.Published)
                .Select(x => x.Id);
            return query.ToList();
        }
    }
}
